#include "VolumeD3Writer.h"
#include "VolumeGridData.h"
#include "OCMain/zip/gzstream.h"
#include "OCMain/Timer.h"
#include "OCMain/OCMath.h"
#include "OCMain/Log.h"
#include "OCVolume/VolumeHeader.h"
#include "OCVolume/VolumeEncoder.h"
#include "immintrin.h"
#include "fp16/fp16.h"

namespace OCPP
{
	namespace Volume
	{
#define EARTH_RADIUS 6378137.f

		float computeWidthInMeter(float latInDegree, float widthInDdegree)
		{
			return cos(Math::AngleUnitsToRadians(latInDegree)) * EARTH_RADIUS * Math::PI * (widthInDdegree / 180.0);
		}

		float computeHeightInMeter(float heightInDegree)
		{
			return EARTH_RADIUS * Math::PI * (heightInDegree / 180.0);

		}
		VolumeD3Writer::VolumeD3Writer()
		{
			_typeExtension = "bin";
		}

		VolumeD3Writer::~VolumeD3Writer()
		{

		}

		

		bool VolumeD3Writer::write(GridData& gridData, const VolumeOptions& options)
		{
			gridData.read(options);
			std::unique_ptr<std::ostream> outStream = getOutStream(gridData, options, true);
			if (outStream == nullptr)
			{
				return false;
			}

			const auto& extent = gridData.extent;
			const auto& vecUint8 = gridData.gridDataValue->_vecUint8;
			const auto& vecFloat = gridData.gridDataValue->_vecFloat;
			auto& vecUint16 = gridData.gridDataValue->_vecUint16;
			auto numElement = vecFloat.size();
			size_t perElementBytes = 1;


			if (!vecUint8.empty())
			{
				numElement = vecUint8.size();
			}
			else if (!vecUint16.empty())
			{
				numElement = vecUint16.size();
				perElementBytes = 2;
			}
			else if (!vecFloat.empty())
			{
				numElement = vecFloat.size();
				perElementBytes = 4;
			}

			size_t bodyLength = numElement * perElementBytes;
			int numComponent = numElement / (extent.dimX * extent.dimY * extent.dimZ);

			int pixelDataType = GL_FLOAT;

			if (!vecUint8.empty())
			{
				pixelDataType = GL_UNSIGNED_BYTE;
				String header = VolumeHeader::generateHeader(gridData,
					bodyLength, numComponent, pixelDataType, options);
				outStream->write(header.data(), header.size());
				outStream->write((char*)&vecUint8[0], bodyLength);
			}
			else if (!vecUint16.empty() || options.preferHalfFloat)
			{
				perElementBytes = 2;
				pixelDataType = GL_HALF_FLOAT_CesiumUse;
				bodyLength = numElement * perElementBytes;

				if (vecUint16.empty())
				{
					vecUint16.resize(numElement);
					for (auto i = 0; i < numElement; i++)
					{
						vecUint16[i] = fp16_ieee_from_fp32_value(vecFloat[i]);
					}
				}
				String header = VolumeHeader::generateHeader(gridData, 
					bodyLength, numComponent, pixelDataType,options);
				outStream->write(header.data(), header.size());
				outStream->write((char*)&vecUint16[0], bodyLength);
			}
			else
			{
				String header = VolumeHeader::generateHeader(gridData, 
					bodyLength, numComponent, pixelDataType, options);
				outStream->write(header.data(), header.size());
				outStream->write((char*)&vecFloat[0], bodyLength);
			}
			

			onWriteDone(*outStream);

			return VolumeWriter::write(gridData, options);
		}

		bool VolumeD3Writer::write(const VolumeGridData& ncGridData, const VolumeOptions& options)
		{
			//setGZipCompress(true);
			_binary = true;

			std::unique_ptr<std::ostream> outStream = getOutStream(ncGridData, options);

			if (outStream == nullptr)
			{
				return false;
			}

			const auto& extent = ncGridData._VolumeExtent;
			float west = extent.xMin;
			float south = extent.yMin;
			float east = extent.xMax;
			float north = extent.yMax;

			int dimZ = extent.dimZ;
			int dimY = extent.dimY;
			int dimX = extent.dimX;

			float width = extent.xMax - extent.xMin;
			float height = extent.yMax - extent.yMin;
			float depth = extent.zMax - extent.zMin;

			float widthInMeters = computeWidthInMeter(south, width);
			float heightInMeters = computeHeightInMeter(height);
			float levelInMeters = dimZ == 1 ? 0.0f : depth * 1000.0;

			VolumeHeader header;
			header.setExtent(west, south, east, north);
			header.setSizeInMeters(widthInMeters, heightInMeters, levelInMeters);
			header.setSizeOfSource(dimX, dimY, dimZ);

			bool isFloatType = ncGridData._vecFloat.size() > 0;
			bool absoluteValue = options.absoluteValue;
			bool hasCustomMaxValue = options.customMaxValue.has_value();

			Uint16Vector vecUint16 = ncGridData._vecUint16;

			if (isFloatType && options.preferHalfFloat) {
				vecUint16.resize(ncGridData._vecFloat.size());
				for (auto i = 0; i < ncGridData._vecFloat.size(); i++)
				{
					float value = ncGridData._vecFloat[i];
					if (absoluteValue) 
					{
						value = abs(value);
					}
					if (hasCustomMaxValue && value > options.customMaxValue.value())
					{
						value = options.customMaxValue.value();
					}
					auto fp16 = fp16_ieee_from_fp32_value(value);
					vecUint16[i] = fp16;
				}
			}

			if (vecUint16.size() != 0) {
				size_t size = vecUint16.size();
				String headerBytes = header.getBytes(size * 2, GL_HALF_FLOAT_CesiumUse);
				outStream->write(headerBytes.data(), headerBytes.size());
				outStream->write((char*)&vecUint16[0], size * 2);
			}
			else if (ncGridData._vecChar.size() != 0)
			{
				size_t size = ncGridData._vecChar.size();
				String headerBytes = header.getBytes(size);
				outStream->write(headerBytes.data(), headerBytes.size());
				outStream->write((char*)&ncGridData._vecChar[0], size);
			}
			else
			{
				size_t size = ncGridData.getSize();
				if (size != dimZ * dimY * dimX || _encodeRange == VolumeEncoder::EncodeRangeNone)
				{
					FloatVector& data = const_cast<FloatVector&>(ncGridData._vecFloat);
					String headerBytes = header.getBytes(data.size() * sizeof(float), GL_FLOAT);
					outStream->write(headerBytes.data(), headerBytes.size());

					float fixValue = getFixValue();
					if (fixValue != 0.0f)
					{
						for (auto i = 0; i < data.size(); i++)
						{
							data[i] += fixValue;
						}
					}

					outStream->write((char*)&data[0], data.size() * sizeof(float));
				}
				else
				{
					int comp = 1;
					Uint8Vector data;
					data.resize(size * comp);

					auto getValue = [&ncGridData, this](int i) {
						float value = ncGridData.getFloat(i);
						if (!ncGridData.isValidValue(value)) {
							value = getFillValue();
						}
						float fixValue = getFixValue();
						return value + fixValue;
						};
#define _OUT_MIN_MAX 1
#if _OUT_MIN_MAX
					float minValue = 1e+5;
					float maxValue = -minValue;
#endif 
					for (int z = 0; z < dimZ; z++)
					{
						int idZ = z * dimY * dimX;
						for (int y = 0; y < dimY; y++)
						{
							int idY = y * dimX;
							for (int x = 0; x < dimX; x++)
							{
								int i = idZ + idY + x;
								float value = getValue(i);
								if (value < minValue) minValue = value;
								if (value > maxValue) maxValue = value;
								data[i * comp] = VolumeEncoder::encodeInRange(value, _encodeRange);
							}
						}
					}
#if _OUT_MIN_MAX
					OCPP_LOG_INFO << "minValue:" << minValue << ",maxValue:" << maxValue << "\n";
#endif 
					String headerBytes = header.getBytes(data.size());
					outStream->write(headerBytes.data(), headerBytes.size());
					outStream->write((char*)&data[0], data.size());
				}
			}

			return VolumeWriter::write(ncGridData, options);
		}
	}
}


